package mtvotingserver;

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;

/**
 * Opens the socket and listens for connections.
 * Also manages the client and voting buffers.
 * Uses thread pools to allow a steady flow of connections.
 * 
 * @author Alex Hrin (0807133)
 */
public class VoteInterpreter extends Thread {

    private ReentrantLock voteOptionsLock;
    private ClientBuffer tempBuffer;
    private ConcurrentMap<String, Integer> voteOptions = 
            new ConcurrentHashMap<String, Integer>();
    private ArrayList<String> voteOptionsBackBuffer = 
            new ArrayList<String>();
    private ConcurrentLinkedDeque<ClientModel> clientHistory = 
            new ConcurrentLinkedDeque<ClientModel>();
    private int clientHistoryRecentIndex = 0;
    private boolean votingInProgress;
    private int connectionPort = 23;
    private int maxThreads = 25;
    private ServerSocket socket = null;
    private ExecutorService threadPoolExecutor = null;

    /**
     * Constructor
     * @param size The size of the active clients buffer will dictate at what 
     *             load a client may be forgotten before confirming the vote in 
     *             a reasonable amount of time.
     */
    public VoteInterpreter(int size) {
        tempBuffer = new ClientBuffer(size);
        threadPoolExecutor = Executors.newFixedThreadPool(maxThreads);
        voteOptionsLock = new ReentrantLock();
    }

    @Override
    public void run() {
        // Raise flag to signal that voting has begun
        votingInProgress = true;
        try {
            // Create the socket
            socket = new ServerSocket(connectionPort);
            while (votingInProgress) {
                try {
                    // Listen for connections
                    Socket newClientSocket = socket.accept();
                    if (newClientSocket != null)
                        // Assign the new client to a ClientRequest instance
                        // and send the instance to the threadpool
                        threadPoolExecutor.execute(
                                new ClientRequest(this, newClientSocket));
                } catch (IOException e) {
                }
            }
        } catch (IOException e) {
        }
    }

    /**
     * Gets the buffer holding all the active clients
     * @return the buffer with the active client connections
     */
    public ClientBuffer getTempBuffer() {
        return tempBuffer;
    }
    
    /**
     * Forces the socket to close and changes the voting flag so the thread can
     * terminate.
     */
    public void stopVoting() {
        votingInProgress = false;
        try {
            socket.close();
        } catch (IOException ex) {
        }
    }

    /**
     * Gets the number of votes for a single option 
     * (or housemate in this instance)
     * @param key the unique string that defines the option
     * @return an integer representing the total votes for the option that are 
     *         counted since the start of the session and up to this moment
     */
    public Integer getVotes(String key) {
        voteOptionsLock.lock();
        Integer votes = 0;
        try {
            votes = voteOptions.get(key);
        } finally {
            voteOptionsLock.unlock();
        }
        return votes;
    }

    /**
     * Gets the a string array with all the options registered on the server
     * @return an array with the data kept in the option back buffer 
     *         (which is just a utility copy of the actual hash map to keep the
     *         initial order of the options)
     */
    public String[] getOptions() {
        return voteOptionsBackBuffer.toArray(new String[0]);
    }
    
    /**
     * Gets the connection history since the last query made to this method
     * @return an array of client model representing all the new clients
     *         that have connected and confirmed their votes
     */
    public ClientModel[] getClientStats() {
        ClientModel[] clientHistoryArray = 
                clientHistory.toArray(new ClientModel[0]);
        // If there are any new clients
        if (clientHistoryRecentIndex < clientHistoryArray.length) {
            // Instantiate an array to hold them
            ClientModel[] clientStats = new ClientModel[
                    clientHistoryArray.length - clientHistoryRecentIndex];
            // Copy the appropriate section of the history to that array
            System.arraycopy(clientHistoryArray, clientHistoryRecentIndex, 
                    clientStats, 0, clientStats.length);
            // Update the history index for the next time this method is called
            clientHistoryRecentIndex = clientHistoryArray.length;
            return clientStats;
        }
        return null;
    }
    
    /**
     * Adds a client to the client history buffer
     * @param client the client to add
     */
    public void pushToClientHistory(ClientModel client) {
        clientHistory.add(client);
    }
    
    /**
     * Searches for a client in the history buffer
     * @param newClient the client holding the IP and port data that needs to be 
     *                  matched
     * @return the client that matches the parameter, if found, null otherwise
     */
    public ClientModel getFromHistory(ClientModel newClient) {
        ClientModel client = null;
        Iterator<ClientModel> iterator = clientHistory.iterator();
        
        // Advance the iterator
        while (iterator.hasNext()) {
            client = iterator.next();
            // Check if the client matches
            if (client.getClientIP().equals(newClient.getClientIP()) && 
                    client.getClientPort() == newClient.getClientPort()) {
                // Return if a match is found
                return client;
            }
        }
        
        // Nothing found
        return null;
    }

    /**
     * Registers a new voting option with the server
     * @param option the string representing the option
     */
    public void addOption(String option) {
        // First clean the string
        option = StringTools.cleanString(option);
        if (option != null) {
            // Add the option to both the hashmap for fast voting access and
            // the back buffer for interface feedback
            voteOptions.putIfAbsent(option, 0);
            voteOptionsBackBuffer.add(option);
        }
    }

    /**
     * Removes a registered option
     * @param option the value of the option to be removed
     * @return the votes that this option got up to the point of being removed
     */
    public Integer removeOption(String option) {
        voteOptionsBackBuffer.remove(option);
        return voteOptions.remove(option);
    }

    /**
     * Adds a vote to a registered option
     * @param clientVote the value of the option for which the vote count
     *                   will be incremented
     */
    public void addVote(String clientVote) {
        Integer votes = voteOptions.get(clientVote);
        voteOptions.replace(clientVote, votes + 1);
    }

    /**
     * Gets the port on which the socket is open
     * @return the port number
     */
    public int getConnectionPort() {
        return connectionPort;
    }

    /**
     * Sets the port on which the socket should be opened
     * @param connectionPort the port number
     */
    public void setConnectionPort(int connectionPort) {
        if (!votingInProgress) {
            this.connectionPort = connectionPort;
        }
    }

    /**
     * Determines if the option is registered with the server or not
     * @param option the value of the voting option
     * @return a boolean indicating if the option has been registered with the
     *         server
     */
    boolean isValidOption(String option) {
        if (option != null) {
            if (voteOptions.containsKey(option)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Clears all registered options
     */
    void clearOptions() {
        voteOptionsBackBuffer.clear();
        voteOptions.clear();
    }
}
